// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/renderer_host/input/tap_suppression_controller.h"

#include "base/logging.h"
#include "base/trace_event/trace_event.h"
#include "content/browser/renderer_host/input/tap_suppression_controller_client.h"
#include "ui/events/gesture_detection/gesture_configuration.h"

namespace content {

// The tapDownTimer is used to avoid waiting for an arbitrarily late fling
// cancel ack. While the timer is running, if a fling cancel ack with
// |Processed = false| arrives, all stashed gesture events get forwarded. If
// the timer expires, the controller forwards stashed GestureTapDown only, and
// drops the rest of the stashed events. The timer delay should be large enough
// for a GestureLongPress to get stashed and forwarded if needed. It's still
// possible for a GestureLongPress to arrive after the timer expiration. In
// this case, it will be suppressed if the controller is in SUPPRESSING_TAPS
// state.

TapSuppressionController::Config::Config()
    : enabled(false)
    , max_cancel_to_down_time(base::TimeDelta::FromMilliseconds(180))
{
    ui::GestureConfiguration* gesture_config = ui::GestureConfiguration::GetInstance();
    max_tap_gap_time = base::TimeDelta::FromMilliseconds(
        gesture_config->long_press_time_in_ms() + 50);
}

TapSuppressionController::TapSuppressionController(
    TapSuppressionControllerClient* client,
    const Config& config)
    : client_(client)
    , state_(config.enabled ? NOTHING : DISABLED)
    , max_cancel_to_down_time_(config.max_cancel_to_down_time)
    , max_tap_gap_time_(config.max_tap_gap_time)
{
}

TapSuppressionController::~TapSuppressionController() { }

void TapSuppressionController::GestureFlingCancel()
{
    switch (state_) {
    case DISABLED:
        break;
    case NOTHING:
    case GFC_IN_PROGRESS:
    case LAST_CANCEL_STOPPED_FLING:
    case SUPPRESSING_TAPS:
        state_ = GFC_IN_PROGRESS;
        break;
    case TAP_DOWN_STASHED:
        break;
    }
}

void TapSuppressionController::GestureFlingCancelAck(bool processed)
{
    base::TimeTicks event_time = Now();
    switch (state_) {
    case DISABLED:
    case NOTHING:
    case SUPPRESSING_TAPS:
        break;
    case GFC_IN_PROGRESS:
        if (processed)
            fling_cancel_time_ = event_time;
        state_ = LAST_CANCEL_STOPPED_FLING;
        break;
    case TAP_DOWN_STASHED:
        if (!processed) {
            TRACE_EVENT0("browser",
                "TapSuppressionController::GestureFlingCancelAck");
            StopTapDownTimer();
            // If the fling cancel is not processed, forward all stashed
            // gesture events.
            client_->ForwardStashedGestureEvents();
            state_ = NOTHING;
        } // Else waiting for the timer to release the stashed tap down.
        break;
    case LAST_CANCEL_STOPPED_FLING:
        break;
    }
}

bool TapSuppressionController::ShouldDeferTapDown()
{
    base::TimeTicks event_time = Now();
    switch (state_) {
    case DISABLED:
    case NOTHING:
        return false;
    case GFC_IN_PROGRESS:
        state_ = TAP_DOWN_STASHED;
        StartTapDownTimer(max_tap_gap_time_);
        return true;
    case TAP_DOWN_STASHED:
        NOTREACHED() << "TapDown on TAP_DOWN_STASHED state";
        state_ = NOTHING;
        return false;
    case LAST_CANCEL_STOPPED_FLING:
        if ((event_time - fling_cancel_time_) < max_cancel_to_down_time_) {
            state_ = TAP_DOWN_STASHED;
            StartTapDownTimer(max_tap_gap_time_);
            return true;
        } else {
            state_ = NOTHING;
            return false;
        }
    // Stop suppressing tap end events.
    case SUPPRESSING_TAPS:
        state_ = NOTHING;
        return false;
    }
    NOTREACHED() << "Invalid state";
    return false;
}

bool TapSuppressionController::ShouldSuppressTapEnd()
{
    switch (state_) {
    case DISABLED:
    case NOTHING:
    case GFC_IN_PROGRESS:
        return false;
    case TAP_DOWN_STASHED:
        // A tap cancel happens before long tap and two finger tap events. To
        // drop the latter events as well as the tap cancel, change the state
        // to "SUPPRESSING_TAPS" when the stashed tap down is dropped.
        state_ = SUPPRESSING_TAPS;
        StopTapDownTimer();
        client_->DropStashedTapDown();
        return true;
    case LAST_CANCEL_STOPPED_FLING:
        NOTREACHED() << "Invalid tap end on LAST_CANCEL_STOPPED_FLING state";
    case SUPPRESSING_TAPS:
        return true;
    }
    return false;
}

base::TimeTicks TapSuppressionController::Now()
{
    return base::TimeTicks::Now();
}

void TapSuppressionController::StartTapDownTimer(const base::TimeDelta& delay)
{
    tap_down_timer_.Start(FROM_HERE, delay, this,
        &TapSuppressionController::TapDownTimerExpired);
}

void TapSuppressionController::StopTapDownTimer()
{
    tap_down_timer_.Stop();
}

void TapSuppressionController::TapDownTimerExpired()
{
    switch (state_) {
    case DISABLED:
    case NOTHING:
    case SUPPRESSING_TAPS:
        NOTREACHED() << "Timer fired on invalid state.";
        break;
    case GFC_IN_PROGRESS:
    case LAST_CANCEL_STOPPED_FLING:
        NOTREACHED() << "Timer fired on invalid state.";
        state_ = NOTHING;
        break;
    case TAP_DOWN_STASHED:
        TRACE_EVENT0("browser",
            "TapSuppressionController::TapDownTimerExpired");
        // When the timer expires, only forward the stashed tap down event, and
        // drop other stashed gesture events (show press or long press).
        client_->ForwardStashedTapDown();
        state_ = SUPPRESSING_TAPS;
        break;
    }
}

} // namespace content
